고차 컴포넌트: HOC(Higher Order Component)

컴포넌트 로직을 재사용하기 위한 React의 고급 기술

고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수이다.

클래스형 컴포넌트에서 고차 컴포넌트 사용의 예

외부로부터 데이터를 구독하여 댓글 목록을 렌더링

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" 는 글로벌 데이터 소스입니다.
      comments: DataSource.getComments(),
    };
  }

  componentDidMount() {
    // 변화감지를 위해 리스너를 추가합니다.
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 리스너를 제거합니다.
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 데이터 소스가 변경될때 마다 comments를 업데이트합니다.
    this.setState({
      comments: DataSource.getComments(),
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

블로그 포스트를 구독하기 위해 위와 비슷한 패턴으로 컴포넌트를 작성

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id),
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id),
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

위 두 컴포넌트는 대부분의 구현체가 동일하다.

  • 컴포넌트가 마운트되면, change 리스너를 DataSource에 추가
  • 리스너 안에서, 데이터 소스가 변경되면 setState를 호출
  • 컴포넌트가 마운트 해제되면 change 리스너를 제거

따라서, DataSource 를 구독하는 CommentListBlogPost 같은 컴포넌트를 생성하는 함수, withSubscription를 만들어 활용할 수 있다.

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments(),
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id),
);

고차 컴포넌트 withSubscription

고차 컴포넌트는 원본 컴포넌트를 컨테이너 컴포넌트로 포장(Wrapping)하여 조합(compose)합니다.

// 이 함수는 컴포넌트를 매개변수로 받고..
function withSubscription(WrappedComponent, selectData) {
  // ...다른 컴포넌트를 반환하는데...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props),
      };
    }

    componentDidMount() {
      // ... 구독을 담당하고...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props),
      });
    }

    render() {
      // ... 래핑된 컴포넌트를 새로운 데이터로 렌더링 합니다!
      // 컴포넌트에 추가로 props를 내려주는 것에 주목하세요.
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

함수형 컴포넌트에서 고차 컴포넌트 사용의 예

function withLoading(Component) {
  const WithLoadingComponent = (props) => {
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      const timer = setTimeout(() => setLoading);
      return () => clearTimeout(timer);
    }, []);

    return loading ? <p>Loading ... </p> : <Component {...props} />;
  };
  return WithLoadingComponent;
}
function Button() {
  return <button>Button</button>;
}

export default withLoading(Button);